Architecture Note

Future - Shared Contract

The safest long-term split is to make a common core own the portable game domain, while Worldshaper owns authoring workflows and World-Workshop owns runtime execution, ECS, rendering, and simulation wiring.

Decision rule: if a module can run unchanged in tests, browser, editor, and runtime without depending on React, Pixi, Tauri, or bitECS, it is a strong candidate for the shared core.

Recommended Ownership Split

Shared Core

Owns the portable contract, validation, normalization, and pure game-domain logic.

  • Authored content types
  • Stable IDs and references
  • Schema versions and migrations
  • Reference resolution
  • Pure gameplay formulas and rule evaluation
  • Prefab and archetype definitions

Runtime

Owns the executable simulation and all performance-driven or transient state.

  • bitECS components and entity storage
  • Spawn and assembly from content to ECS
  • Systems, ticking, AI, pathfinding integration
  • Rendering, audio, input, camera
  • Runtime caches and live state

Editor

Owns source editing UX, layout, tools, and authoring-specific productivity features.

  • Inspectors, forms, palettes, map editing
  • TS source rewriting and formatting
  • Undo/redo, selection, layout preferences
  • Authoring warnings and quick fixes
  • Studio-only metadata and workflows

What Belongs In The Shared Core

Area Safe To Share Why
Content definitions World, Chunk, NpcTemplate, PlacedEntity, Ability, Dialogue, Item These are authored domain concepts, not renderer or ECS details.
Type helpers defineNpc(), defineWorld(), definePrefab() Both editor and runtime benefit from a single shape and single set of defaults.
IDs and refs NpcId, WorldId, DialogueId, SpriteId, TemplateId Stable identifiers should mean the same thing everywhere.
Validation Reference checks, required fields, defaulting, normalization, semantic validation Content should be judged by one source of truth, not two slightly different validators.
Migrations schemaVersion upgrades and canonical output transforms This keeps old content loadable in both tools.
Pure rules Dialogue condition checks, loot rolls, stat formulas, cooldown math, faction logic These are deterministic rules that do not need the runtime shell.
Content registry Lookup and resolution APIs over loaded definitions Both projects need the same view of the authored world.
Shared core example: portable authored contractexport interface PlacedEntity {
  id: EntityInstanceId;
  templateId?: EntityTemplateId;
  archetype: ArchetypeId;
  position: { x: number; y: number; z: number };
  spriteId?: SpriteId;
  dialogueId?: DialogueId;
  factionId?: FactionId;
  tags?: string[];
  overrides?: Partial<EntityTemplate>;
}

export function definePlacedEntity(
  value: PlacedEntity,
): PlacedEntity {
  return normalizePlacedEntity(value);
}

What Should Stay Runtime-Owned

The runtime should keep everything that is shaped primarily for execution speed, frame-to-frame state, ECS storage layout, or engine integration.

Keep These In World-Workshop

  • bitECS component arrays and entity IDs
  • ECS world creation and system scheduling
  • Assembly from authored entities into spawned ECS entities
  • Transient state like routes, targets, planner handles, and sprite instances
  • Rendering, audio, input, camera, spatial index, pathfinder integration

Why

  • These are not stable authoring concepts.
  • They will change when the engine changes.
  • They often depend on engine libraries and performance tradeoffs.
  • They should be free to evolve without forcing content migrations.
Runtime example: ECS remains runtime-onlyconst GridPosition = {
  x: new Int16Array(MAX_ENTITIES),
  y: new Int16Array(MAX_ENTITIES),
  z: new Int16Array(MAX_ENTITIES),
};

const Target = {
  x: new Int16Array(MAX_ENTITIES),
  y: new Int16Array(MAX_ENTITIES),
  z: new Int16Array(MAX_ENTITIES),
  active: new Uint8Array(MAX_ENTITIES),
};

function spawnFriendlyNpc(world: GameWorld, def: ResolvedEntityDef): number {
  const entity = addEntity(world);
  addComponent(world, entity, GridPosition);
  addComponent(world, entity, Target);
  addComponent(world, entity, Agent);
  // Runtime decides the exact ECS shape here.
  return entity;
}

What Should Stay Editor-Owned

The editor should keep everything that exists to improve authoring speed, visibility, and safety, especially if it is tied to UI behavior or source editing mechanics.

Important: do not move UI state or source-file IO details into the shared core. Those will create coupling fast and usually provide very little reuse value.

The Boundary To Protect

The shared core should describe what the authored thing is. The runtime should decide how that thing becomes executable ECS state.

Good Core Concepts

  • EntityTemplate
  • PlacedEntity
  • ArchetypeId
  • BehaviorId
  • StatBlock
  • SpawnRule
  • VisualRef

Bad Core Concepts

  • GridPosition typed arrays
  • RenderPosition
  • Target.active
  • Planner handles
  • Sprite instances
  • Route caches
  • Per-frame live ECS state
The bridge: authored data into runtime assembly// Shared core
export interface EntityTemplate {
  id: EntityTemplateId;
  archetype: ArchetypeId;
  spriteId?: SpriteId;
  stats?: StatBlock;
  dialogueId?: DialogueId;
}

// Runtime
export function materializeEntity(
  world: GameWorld,
  entity: ResolvedPlacedEntity,
): number {
  switch (entity.archetype) {
    case "friendly_npc":
      return spawnFriendlyNpc(world, entity);
    case "monster":
      return spawnMonster(world, entity);
    case "player_spawn":
      return spawnPlayerMarker(world, entity);
  }
}

Tempting, But Usually Not Worth Sharing

Raw file IO: how a TS file is discovered, parsed, rewritten, and formatted should usually stay editor-side or build-side.
React components: UI does not become more reusable just because it sits in a shared package.
Pixi renderer code: renderer concerns are runtime concerns.
ECS storage: component arrays and world memory layout are engine implementation details.
Editor state: selections, panels, and tool modes are not game-domain concepts.
Live save-state internals: runtime persistence format may overlap with content, but live simulation state should stay runtime-owned.

Suggested Package Shape

A practical next-step layoutpackages/
  world-core/
    src/
      ids/
      content/
      define/
      validate/
      normalize/
      migrate/
      resolve/
      rules/
      registry/

  world-content-build/
    src/
      loadTsModules/
      compileBundle/
      emitArtifacts/

Worldshaper/
  src/
    editor-ui/
    source-editing/
    tooling/

World-Workshop/
  src/
    game/
      ecs/
      systems/
      runtime/
      rendering/
      assembly/

Low-Risk Extraction Order

  1. Extract IDs, record types, and defineX() helpers.
  2. Extract validation, normalization, and migrations.
  3. Extract reference resolution and a shared content registry.
  4. Extract pure rules used by both projects.
  5. Leave ECS assembly, rendering, and editor source-editing mechanics where they are.
Why this order works: it gives immediate reuse and consistency without forcing an early, risky merge of the two apps' most specialized code.